<!doctype html>
<meta charset=utf-8>
<link rel=stylesheet href=../css/reset.css>
<title>Generator for editing conformance tests</title>
<p>See the <a href=editing.html#tests>Tests</a> section of the specification
for documentation.

<p><button onclick="generateTests(); this.parentNode.removeChild(this)">Generate and submit tests</button>

<p id=timing></p>

<ul id=errors></ul>

<form action=submit.php method=post>
<input type=hidden name=elapsed>
<input type=hidden name=output>
<input type=hidden name=errors>
<p><label><input type=radio name=ua value=gecko> Gecko</label>
   <label><input type=radio name=ua value=webkit> WebKit</label>
<!-- Username is ignored, it's just so browsers offer to remember the password.
-->
<p><label>Username: <input value=dummyuser name=username></label>
<p><label>Password: <input type=password name=password></label>
</form>

<div id=test-container></div>

<script src=../implementation.js></script>
<script>var testsJsLibraryOnly = true</script>
<script src=../tests.js></script>
<script>
"use strict";

if (/WebKit/.test(navigator.userAgent)) {
	document.querySelector("[type=radio][value=webkit]").checked = true;
} else if (/Gecko\//.test(navigator.userAgent)) {
	document.querySelector("[type=radio][value=gecko]").checked = true;
}

var originalTitle = document.title;

var testOutput = '';

function generateTests() {
	var startTime = Date.now();
	document.getElementById("test-container").innerHTML = "<div contenteditable></div><p>test";

	testOutput = "var browserTests = [\n";
	for (var command in tests) {
		document.title = "(" + command + ") " + originalTitle;
		tests[command].forEach(function(test) {
			if (badTests[command].indexOf(test) != -1) {
				return;
			}
			try {
				var normalizedTest = normalizeTest(command, test);
				if (command == "multitest") {
					outputTest(generateTest(normalizedTest));
				} else {
					// Test all permutations of styleWithCSS and
					// defaultParagraphSeparator.  If varying them doesn't look
					// like it does anything interesting, don't output the
					// variation -- this cuts down hugely on the number of
					// tests.
					var testVariants = [];
					normalizedTest.splice(1, 0, ["stylewithcss", "true"],
						["defaultparagraphseparator", "div"]);
					testVariants.push(generateTest(normalizedTest));
					normalizedTest.splice(1, 2, ["stylewithcss", "false"],
						["defaultparagraphseparator", "div"]);
					testVariants.push(generateTest(normalizedTest));
					normalizedTest.splice(1, 2, ["stylewithcss", "true"],
						["defaultparagraphseparator", "p"]);
					testVariants.push(generateTest(normalizedTest));
					normalizedTest.splice(1, 2, ["stylewithcss", "false"],
						["defaultparagraphseparator", "p"]);
					testVariants.push(generateTest(normalizedTest));

					var styleWithCssPatterns = [
						/<b[^a-zA-Z0-9]/,
						/<i[^a-zA-Z0-9]/,
						/<u[^a-zA-Z0-9]/,
						/<s[^a-zA-Z0-9]/,
						/<strike[^a-zA-Z0-9]/,
						/<font[^a-zA-Z0-9]/,
						/<sub[^a-zA-Z0-9]/,
						/<sup[^a-zA-Z0-9]/,
						/style=/,
					];
					var defaultParagraphSeparatorPatterns = [
						/<p[^a-zA-Z0-9]/,
						/<div[^a-zA-Z0-9]/,
					];
					var ignoreStyleWithCSS = false;
					var ignoreDefaultParagraphSeparator = false;

					if (testVariants[0][2] == testVariants[1][2]
					&& styleWithCssPatterns.every(function(re) {
						return testVariants[0][2].split(re).length
							== testVariants[0][0].split(re).length;
					})
					&& testVariants[2][2] == testVariants[3][2]
					&& styleWithCssPatterns.every(function(re) {
						return testVariants[2][2].split(re).length
							== testVariants[2][0].split(re).length;
					})) {
						ignoreStyleWithCSS = true;
					}

					if (testVariants[0][2] == testVariants[2][2]
					&& defaultParagraphSeparatorPatterns.every(function(re) {
						return testVariants[0][2].split(re).length
							== testVariants[0][0].split(re).length;
					})
					&& testVariants[1][2] == testVariants[3][2]
					&& defaultParagraphSeparatorPatterns.every(function(re) {
						return testVariants[1][2].split(re).length
							== testVariants[1][0].split(re).length;
					})) {
						ignoreDefaultParagraphSeparator = true;
					}

					if (ignoreStyleWithCSS && ignoreDefaultParagraphSeparator) {
						testVariants = [testVariants[0]];
						testVariants[0][1].splice(0, 2);
						testVariants[0][3].splice(0, 2);
					} else if (ignoreStyleWithCSS) {
						testVariants = [testVariants[0], testVariants[2]];
						testVariants[0][1].splice(0, 1);
						testVariants[0][3].splice(0, 1);
						testVariants[1][1].splice(0, 1);
						testVariants[1][3].splice(0, 1);
					} else if (ignoreDefaultParagraphSeparator) {
						testVariants = [testVariants[0], testVariants[1]];
						testVariants[0][1].splice(1, 1);
						testVariants[0][3].splice(1, 1);
						testVariants[1][1].splice(1, 1);
						testVariants[1][3].splice(1, 1);
					}
					for (var i = 0; i < testVariants.length; i++) {
						if (!testVariants[i][1].some(function(arr) {
							return arr[0] == "stylewithcss";
						})) {
							delete testVariants[i][4].stylewithcss;
						}
						if (!testVariants[i][1].some(function(arr) {
							return arr[0] == "defaultparagraphseparator";
						})) {
							delete testVariants[i][4].defaultparagraphseparator;
						}
						outputTest(testVariants[i]);
					}
				}
			} catch(e) {
				var errorItem = document.createElement("li");
				errorItem.textContent = "Exception, "
					+ command + " " + JSON.stringify(test) + ": "
					+ formatException(e);
				document.getElementById("errors").appendChild(errorItem);
			}
		});
	}

	document.title = "(submitting) " + originalTitle;

	// Finish up our JSON.  First remove the last comma, since JSON doesn't
	// like that, then add a closing bracket.
	testOutput = testOutput.substr(0, testOutput.lastIndexOf(","))
		+ testOutput.substr(1 + testOutput.lastIndexOf(","))
		+ "]\n";
	document.querySelector("input[name=output]").value = testOutput;
	document.querySelector("input[name=elapsed]").value =
		Math.round(Date.now() - startTime);
	document.querySelector("input[name=errors]").value =
		document.querySelector("#errors").innerHTML;

	document.getElementById("test-container").parentNode
		.removeChild(document.getElementById("test-container"));
	document.querySelector("form").submit();
}

// Helper function for generateTest()
function setQueryResult(arr, idx, callback, command, range) {
	var desc, type, result;
	if (callback == myQueryCommandIndeterm) {
		desc = 'myQueryCommandIndeterm("' + command + '")';
		type = "boolean";
	} else if (callback == myQueryCommandState) {
		desc = 'myQueryCommandState("' + command + '")';
		type = "boolean";
	} else if (callback == myQueryCommandValue) {
		desc = 'myQueryCommandValue("' + command + '")';
		type = "string";
	} else {
		throw "Invalid callback";
	}
	try {
		result = callback(command, range);
	} catch (e) {
		if (e !== "INVALID_ACCESS_ERR") {
			throw "Bad exception " + e + " for " + desc;
		}
		arr[idx] = null;
		return;
	}
	if (typeof result !== type) {
		throw desc + " returned result of type " + typeof result + ", expected " + type;
	}
	if (callback == myQueryCommandValue
	&& (command == "backcolor" || command == "forecolor" || command == "hilitecolor")) {
		result = normalizeColor(result);
	}
	arr[idx] = result;
}

/**
 * Input is in the format generated by normalizeTest:
 *   [input HTML, [command, value], [command, value], . . .]
 * Converts to something of the following form:
 *   [input HTML,
 *    array of commands,
 *    expected output HTML,
 *    array of expected execCommand() return values,
 *    object of expected indeterm/state/value].
 * The array of commands is [[command, value], [command, value], ...].  The
 * array of expected execCommand() return values is [true|false, true|false,
 * ...], where the indices match those in the array of commands.  The
 * indeterm/state/value object is of the form
 *   {command: [expected indeterm before, expected state before,
 *     expected value before, expected indeterm after,
 *     expected state after, expected value after],
 *   command: ... }
 * null for any of the last six entries means an INVALID_ACCESS_ERR must be
 * raised.
 *
 * The return value is an array that can be passed to outputTest.
 */
function generateTest(test) {
	var testDiv = document.querySelector("div[contenteditable]");
	var points = setupDiv(testDiv, test[0]);

	// Hack around Firefox bug when generating the tests, although we don't
	// want to give it a pass when actually running them:
	// https://bugzilla.mozilla.org/show_bug.cgi?id=618737
	[].forEach.call(testDiv.querySelectorAll("video[tabindex]"), function(video) {
		if (video.tabIndex == 0) {
			video.removeAttribute("tabindex");
		}
	});

	var range = document.createRange();
	range.setStart(points[0], points[1]);
	range.setEnd(points[2], points[3]);
	// The points might be backwards
	if (range.collapsed) {
		range.setEnd(points[0], points[1]);
	}

	// Verify that we aren't touching any non-editable nodes
	var nonEditable = getDescendants(testDiv).filter(function(node) {
		return !isEditable(node);
	});
	var nonEditableClones = nonEditable.map(function(node) {
		return node.cloneNode(false);
	});

	var expectedQueryResults = {};
	for (var i = 1; i < test.length; i++) {
		if (test[i][0] in expectedQueryResults) {
			continue;
		}
		var results = [];
		setQueryResult(results, 0, myQueryCommandIndeterm, test[i][0], range);
		setQueryResult(results, 1, myQueryCommandState, test[i][0], range);
		setQueryResult(results, 2, myQueryCommandValue, test[i][0], range);
		expectedQueryResults[test[i][0]] = results;
	}

	var expectedExecCommandReturnValues = [];
	for (var i = 1; i < test.length; i++) {
		expectedExecCommandReturnValues.push(
			myExecCommand(test[i][0], false, test[i][1], range));
	}

	for (var command in expectedQueryResults) {
		var results = expectedQueryResults[command];
		setQueryResult(results, 3, myQueryCommandIndeterm, command, range);
		setQueryResult(results, 4, myQueryCommandState, command, range);
		setQueryResult(results, 5, myQueryCommandValue, command, range);
	}

	for (var i = 0; i < nonEditable.length; i++) {
		if (!isDescendant(nonEditable[i], testDiv)) {
			throw "Non-editable node " + i + " is no longer descendant of test div: "
				+ nonEditable[i];
		}
		if (!nonEditableClones[i].isEqualNode(nonEditable[i].cloneNode(false))) {
			throw "Non-editable node " + i + " has changed: "
				+ nonEditable[i];
		}
	}

	var compareDiv1 = testDiv.cloneNode(true);

	// Now do various sanity checks, and throw if they're violated.  First
	// just count children:
	if (testDiv.parentNode.childNodes.length != 2) {
		throw "The cell didn't have two children.  Did something spill outside the test div?";
	}

	// Now verify that the DOM serializes.
	compareDiv1.normalize();
	var compareDiv2 = compareDiv1.cloneNode(false);
	compareDiv2.innerHTML = compareDiv1.innerHTML;
	// Oddly, IE9 sometimes produces two nodes that return true for
	// isEqualNode but have different innerHTML (omitting closing tags vs.
	// not).
	if (!compareDiv1.isEqualNode(compareDiv2)
	&& compareDiv1.innerHTML != compareDiv2.innerHTML) {
		throw "DOM does not round-trip through serialization!  "
			+ prettyPrint(compareDiv1.innerHTML) + " vs. " + prettyPrint(compareDiv2.innerHTML);
	}
	if (!compareDiv1.isEqualNode(compareDiv2)) {
		throw "DOM does not round-trip through serialization (although innerHTML is the same)!  "
			+ prettyPrint(compareDiv1.innerHTML);
	}

	// Check for attributes
	if (testDiv.attributes.length != 1) {
		throw "Wrapper div has extra attributes!  " +
			prettyPrint(testDiv.parentNode.innerHTML);
	}

	// Final sanity check: make sure everything isAllowedChild() of its
	// parent.
	getDescendants(testDiv).forEach(function(descendant) {
		if (!isAllowedChild(descendant, descendant.parentNode)) {
			throw "Something here is not an allowed child of its parent: " + descendant;
		}
	});

	normalizeSerializedStyle(testDiv);

	addBrackets(range);

	return [test[0], test.slice(1), testDiv.innerHTML,
		expectedExecCommandReturnValues, expectedQueryResults];
}

/**
 * Input is in the format generated by generateTest().  This will append the
 * test to testOutput.
 */
function outputTest(arr) {
	testOutput += "["
		+ arr.map(JSON.stringify).join(",\n\t")
		+ "],\n";
}
</script>
